package cn.com.w.simple.demo.filter;

import cn.com.w.simple.demo.entity.LoginResult;
import cn.com.w.simple.demo.entity.UserInfo;
import cn.com.w.simple.demo.service.AbstractLoginHandler;
import cn.com.w.simple.demo.service.AuthenticationToken;
import cn.com.w.simple.demo.vo.RequestResult;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;


/**
 * UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter针对使用用户名和密码进行身份认证而定制化的一个过滤器。
 * 过滤器的实现在调用http.formLogin()时作用，默认的登录请求pattern为"/login"，并且为POST请求。
 * 当我们登录的时候，也就是匹配到loginProcessingUrl，这个过滤器就会委托认证管理器authenticationManager来验证登录
 * <p>
 * AbstractAuthenticationProcessingFilter判断当前路径是否是指定的登陆路径，如果不是，那么跳过此过滤器
 * UsernamePasswordAuthenticationFilter默认登陆路径是/login，post接口
 */
@Slf4j
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter implements Order {
    @Override
    public int value() {
        return 1;
    }

    @Override
    public Class<? extends Annotation> annotationType() {
        return null;
    }

    private AuthenticationManager authenticationManager;

    private AbstractLoginHandler loginHandler;

    /**
     * 主要应用于登录失败时无法获取登录的用户信息
     */
    ThreadLocal<UserInfo> userThreadLocal = new ThreadLocal<>();

    /**
     * 通过构造方法注入 authenticationManager 和 loginHandler
     * 不同的loginHandler实现代表不同的登录方式(策略)
     *
     * @param authenticationManager
     * @param loginHandler          登录处理器
     */
    public CustomLoginFilter(AuthenticationManager authenticationManager, AbstractLoginHandler loginHandler) {
        this.authenticationManager = authenticationManager;
        this.loginHandler = loginHandler;
    }

    /**
     * 尝试开始授权,并返回授权对象
     * <p>
     * Authentication：存储了认证信息，代表当前登录用户
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //return super.attemptAuthentication(request, response);

        /**
         * 这个上下文对象(当前登录用户)则是交由 SecurityContextHolder 进行管理，可以在程序任何地方使用它
         * 这种在一个线程中横跨若干方法调用，需要传递的对象，我们通常称之为上下文（Context）。上下文对象是非常有必要的，否则你每个方法都得额外增加一个参数接收对象，实在太麻烦了。
         * SecurityContextHolder原理非常简单，使用ThreadLocal来保证一个线程中传递同一个对象
         */
        log.info("用户身份上下文:{}", JSONObject.toJSONString(SecurityContextHolder.getContext().getAuthentication()));

        Authentication result = null;
        try {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            System.out.println("当前登录:" + username + ", 密码:" + password);

            UserInfo userInfo = loginHandler.getUserByLoginName(username);
            if (userInfo == null) {
                setRespose(response, HttpServletResponse.SC_OK, "登录用户不存在，请查看您的账号", null, LoginResult.LOGIN_STUTAS_FAIL);
            }

            /**
             * 用户存在,下面要进行验证密码是否正确,有两种方式:1、自己编写验证密码逻辑;2、使用Spring Security组件
             * 方式1:
             * if (userInfo.getLoginPassword().equals(password)) {
             *                 // 密码验证通过
             *                  AuthenticationToken authRequest = new AuthenticationToken(username, password);
             *                  SecurityContextHolder.getContext().setAuthentication(authRequest);
             *             }
             * 方式2:
             * authenticationManager.authenticate(authRequest);
             *
             * AuthenticationManager 就是Spring Security用于执行身份验证的组件，只需要调用它的authenticate方法即可完成认证。
             * Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中调用这个组件，该过滤器负责认证逻辑。
             */
            AuthenticationToken authRequest = new AuthenticationToken(username, password);

            /**
             * AuthenticationManager的校验逻辑：
             * 1、UserDetialsService的实现类通过loadUserByUsername(String username)方法,根据用户名先查询出用户对象(没有查到则抛出异常) 将用户对象的密码和传递过来的密码进行校验，密码不匹配则抛出异常
             *
             * 调用链路
             * ProviderManager.authenticate()
             * AbstractUserDetailsAuthenticationProvider.retrieveUser()
             * DaoAuthenticationProvider.retrieveUser()
             */
            result = authenticationManager.authenticate(authRequest);

            log.info("认证后的Authentication:{}", JSONObject.toJSONString(result));
            log.info("用户身份上下文:{}", JSONObject.toJSONString(SecurityContextHolder.getContext().getAuthentication()));
        } catch (BadCredentialsException e ) {
            setRespose(response, HttpServletResponse.SC_OK, "密码错误", null, LoginResult.LOGIN_STUTAS_ERROR);
        } catch (Exception e) {
            log.error("身份认证失败:{}", e.getMessage(), e);
            setRespose(response, HttpServletResponse.SC_OK, "登录出现未知错误！，请联系管理员进行查询", null, LoginResult.LOGIN_STUTAS_ERROR);
        }
        return result;
    }


    /**
     * 设置返回结果
     *
     * @param response
     */
    private void setRespose(HttpServletResponse response, int status, String message, String authorization, int loginStutas) {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        PrintWriter out = null;
        try {
            out = response.getWriter();
        } catch (IOException ex) {
            log.error("", ex);
        }
        out.write(JSONObject.toJSONString(RequestResult.ok(new LoginResult().setAuthorization(authorization).setMessage(message).setLoginStutas(loginStutas))));
        out.flush();
        out.close();
    }


}



